2 * Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
17 import NCDesktopClientSocketKit
19 import NextcloudFileProviderKit
22 let AuthenticationTimeouts: [UInt64] = [ // Have progressively longer timeouts to not hammer server
23 3_000_000_000, 6_000_000_000, 30_000_000_000, 60_000_000_000, 120_000_000_000, 300_000_000_000
26 extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInterface {
28 This FileProviderExtension extension contains everything needed to communicate with the client.
29 We have two systems for communicating between the extensions and the client.
31 Apple's XPC based File Provider APIs let us easily communicate client -> extension.
32 This is what ClientCommunicationService is for.
34 We also use sockets, because the File Provider XPC system does not let us easily talk from
36 We need this because the extension needs to be able to request account details. We can't
37 reliably do this via XPC because the extensions get torn down by the system, out of the control
38 of the app, and we can receive nil/no services from NSFileProviderManager. Once this is done
41 func supportedServiceSources(
42 for itemIdentifier: NSFileProviderItemIdentifier,
43 completionHandler: @escaping ([NSFileProviderServiceSource]?, Error?) -> Void
45 Logger.desktopClientConnection.debug("Serving supported service sources")
46 let clientCommService = ClientCommunicationService(fpExtension: self)
47 let fpuiExtService = FPUIExtensionServiceSource(fpExtension: self)
48 let services: [NSFileProviderServiceSource] = [clientCommService, fpuiExtService]
49 completionHandler(services, nil)
50 let progress = Progress()
51 progress.cancellationHandler = {
52 let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError)
53 completionHandler(nil, error)
58 @objc func sendFileProviderDomainIdentifier() {
59 let command = "FILE_PROVIDER_DOMAIN_IDENTIFIER_REQUEST_REPLY"
60 let argument = domain.identifier.rawValue
61 let message = command + ":" + argument + "\n"
62 socketClient?.sendMessage(message)
65 private func signalEnumeratorAfterAccountSetup() {
66 guard let fpManager = NSFileProviderManager(for: domain) else {
67 Logger.fileProviderExtension.error(
68 "Could not get file provider manager for domain \(self.domain.displayName, privacy: .public), cannot notify after account setup"
73 assert(ncAccount != nil)
75 fpManager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in
77 Logger.fileProviderExtension.error(
78 "Error resolving not authenticated, received error: \(error!.localizedDescription)"
83 Logger.fileProviderExtension.debug(
84 "Signalling enumerators for user \(self.ncAccount!.username) at server \(self.ncAccount!.serverUrl, privacy: .public)"
91 guard let fpManager = NSFileProviderManager(for: domain) else {
92 Logger.fileProviderExtension.error(
93 "Could not get file provider manager for domain \(self.domain.displayName, privacy: .public), cannot notify changes"
98 fpManager.signalEnumerator(for: .workingSet) { error in
100 Logger.fileProviderExtension.error(
101 "Error signalling enumerator for working set, received error: \(error!.localizedDescription, privacy: .public)"
107 @objc func setupDomainAccount(
108 user: String, userId: String, serverUrl: String, password: String
111 let authTestNcKit = NextcloudKit()
112 authTestNcKit.setup(user: user, userId: userId, password: password, urlBase: serverUrl)
113 var authAttemptState = AuthenticationAttemptResultState.connectionError // default
115 // Retry a few times if we have a connection issue
116 for authTimeout in AuthenticationTimeouts {
117 authAttemptState = await authTestNcKit.tryAuthenticationAttempt()
118 guard authAttemptState == .connectionError else { break }
120 Logger.fileProviderExtension.info(
121 "\(user, privacy: .public) authentication try timed out. Trying again soon."
123 try? await Task.sleep(nanoseconds: authTimeout)
126 switch (authAttemptState) {
127 case .authenticationError:
128 Logger.fileProviderExtension.info(
129 "\(user, privacy: .public) authentication failed due to bad creds, stopping"
132 case .connectionError:
133 // Despite multiple connection attempts we are still getting connection issues.
134 // Connection error should be provided
135 Logger.fileProviderExtension.info(
136 "\(user, privacy: .public) authentication try failed, no connection."
140 Logger.fileProviderExtension.info(
142 Authenticated! Nextcloud account set up in File Provider extension.
143 User: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)
150 Account(user: user, id: userId, serverUrl: serverUrl, password: password)
151 guard newNcAccount != ncAccount else { return }
152 ncAccount = newNcAccount
154 account: newNcAccount.ncKitAccount,
155 user: newNcAccount.username,
156 userId: newNcAccount.id,
157 password: newNcAccount.password,
158 urlBase: newNcAccount.serverUrl,
159 userAgent: "Nextcloud-macOS/FileProviderExt",
160 nextcloudVersion: 25,
161 delegate: nil) // TODO: add delegate methods for self
163 changeObserver = RemoteChangeObserver(
164 remoteInterface: ncKit, changeNotificationInterface: self, domain: domain
166 ncKit.setup(delegate: changeObserver)
167 signalEnumeratorAfterAccountSetup()
172 @objc func removeAccountConfig() {
173 Logger.fileProviderExtension.info(
174 "Received instruction to remove account data for user \(self.ncAccount!.username, privacy: .public) at server \(self.ncAccount!.serverUrl, privacy: .public)"
179 func updatedSyncStateReporting(oldActions: Set<UUID>) {
182 guard oldActions.isEmpty != syncActions.isEmpty else {
187 let command = "FILE_PROVIDER_DOMAIN_SYNC_STATE_CHANGE"
188 var argument: String?
189 if oldActions.isEmpty, !syncActions.isEmpty {
190 argument = "SYNC_STARTED"
191 } else if !oldActions.isEmpty, syncActions.isEmpty {
192 argument = errorActions.isEmpty ? "SYNC_FINISHED" : "SYNC_FAILED"
198 guard let argument else { return }
199 Logger.fileProviderExtension.debug("Reporting sync \(argument)")
200 let message = command + ":" + argument + "\n"
201 socketClient?.sendMessage(message)